using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using TestRunner.Properties;

[Serializable]
public class TestDiscoveryHelper
{
    internal const string DiscoverTestCommand = "/DiscoverTest";
    internal static List<string> GetTestsInAssembly(string assemblyPath, string nunitDirectory)
    {
        string testListFile = Path.GetTempFileName() + Guid.NewGuid() + Guid.NewGuid();
        testListFile = Path.Combine(Path.GetTempPath(), testListFile);
        string[] args = new[] { DiscoverTestCommand, assemblyPath, nunitDirectory, testListFile };
        
        AppDomain domain = AppDomain.CreateDomain("FindAssembly");
        domain.ExecuteAssembly(Process.GetCurrentProcess().MainModule.FileName, args);
        AppDomain.Unload(domain);
        
        if (File.Exists(testListFile))
        {
            var lines = File.ReadAllLines(testListFile).ToList();

            try
            {
                File.Delete(testListFile);
            }
            catch (IOException)
            {
                
            }
            return lines;
        }
        
        throw new InvalidOperationException("Failed to load the test list.");
    }
    
    #region Logic to find the Tests
    private const string VersionPlacement = "Version=";
    private const string CulturePlacement = "Culture=";
    private const string PublicKeyPlacement = "PublicKeyToken=";

    private static List<DependentAssemblyInfo> DependentAssemblies;
    private static List<string> NUnitDirs;
    private static string NUnitDirectory;

    internal static void FindTestsInAssemblyInternal(string assemblyFullPath, string nunitDirectory, string outputFile)
    {
        var tests = FindTestsInAssemblyInternal(assemblyFullPath, nunitDirectory);
        File.WriteAllLines(outputFile, tests.ToArray());
    }

    private static List<string> FindTestsInAssemblyInternal(string assemblyFullPath, string nunitDirectory)
    {
        NUnitDirectory = nunitDirectory;

        List<string> tests = new List<string>();
        
        string configFile = assemblyFullPath + ".config";
        string currentDir = Environment.CurrentDirectory;
        try
        {
            Environment.CurrentDirectory = Path.GetDirectoryName(assemblyFullPath);
            if (File.Exists(configFile))
            {
                SetupAssemblyResolverFromAssemblyBindings(configFile);
            }

            Assembly assembly = Assembly.LoadFile(assemblyFullPath);
            Type[] typess = assembly.GetTypes();
            foreach (Type type in typess.Where(p => p.IsClass && (p.IsAbstract == false || p.IsSealed)))
            {
                var attributes = type.GetCustomAttributes(true);
                foreach (object attribute in attributes)
                {
                    if (attribute.GetType().FullName == "NUnit.Framework.TestFixtureAttribute")
                    {
                        tests.Add(type.FullName);
                        break;
                    }
                }
            }
            return tests.OrderBy(p => p).ToList();
        }
        catch (ReflectionTypeLoadException ex)
        {
            throw ex.LoaderExceptions[0];
        }
        finally
        {
            Environment.CurrentDirectory = currentDir;
            AppDomain.CurrentDomain.AssemblyResolve -= OnResolveAssembly;
            DependentAssemblies = null;
        }
    }

    private static void SetupAssemblyResolverFromAssemblyBindings(string configFile)
    {
        DependentAssemblies = new List<DependentAssemblyInfo>();

        using (StringReader inputSource = new StringReader(File.ReadAllText(configFile)))
        {
            using (XmlTextReader reader = new XmlTextReader(inputSource))
            {
                reader.WhitespaceHandling = WhitespaceHandling.None;
                //string assemblyName = "";
                while (!reader.EOF)
                {
                    if (reader.NodeType == XmlNodeType.Element && reader.Name == "dependentAssembly")
                    {
                        DependentAssemblyInfo assembly = GetDependentAssembly(reader);
                        if (assembly != null)
                        {
                            DependentAssemblies.Add(assembly);
                            continue;
                        }
                    }
                    reader.Read();
                }
            }
        }

        if (DependentAssemblies.Count > 0)
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
        }

        NUnitDirs = new List<string>();
        if (Directory.Exists(NUnitDirectory))
        {
            NUnitDirs.Add(NUnitDirectory);
            NUnitDirs.Add(Path.Combine(NUnitDirectory, "framework"));
            NUnitDirs.Add(Path.Combine(NUnitDirectory, "framework"));
            NUnitDirs.Add(Path.Combine(NUnitDirectory, "lib"));
        }
    }

    static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
    {
        //Trace.WriteLine("Attempting to resolve: " + args.Name);

        var parts = args.Name.Split(',');
        if (parts.Length < 4) return null;
        var name = parts[0];
        string version = GetPartIfAvailable(parts[1], VersionPlacement);
        string culture = GetPartIfAvailable(parts[2], CulturePlacement);
        string publicKey = GetPartIfAvailable(parts[3], PublicKeyPlacement);

        var assemblies = DependentAssemblies.Where(p => String.Compare(p.Name, name, StringComparison.OrdinalIgnoreCase) == 0 && File.Exists(p.SuggestedPath)).ToList();
        if (assemblies.Count == 0)
        {
            //Trace.WriteLine("NOT FOUND: " + args.Name);
            return FindAssemblyInCurrentDir(name);
        }

        var exactMatch = assemblies.FirstOrDefault(p =>
            new Version(p.Version) == new Version(version)
            &&
            String.Compare(p.Culture, culture, StringComparison.OrdinalIgnoreCase) == 0
            &&
            String.Compare(p.PublicKey, publicKey, StringComparison.OrdinalIgnoreCase) == 0
            );
        if (exactMatch != null)
        {
            //Trace.WriteLine("FOUND: " + args.Name + " ==> Loading from " + exactMatch.SuggestedPath);
            var ass = Assembly.LoadFile(exactMatch.SuggestedPath);

            if (ass != null)
            {
                //Trace.WriteLine("RETURNING: " + args.Name + " ==> Loading from " + exactMatch.SuggestedPath);
                return ass;
            }
        }

        //Trace.WriteLine("NOT FOUND: " + args.Name);
        return FindAssemblyInCurrentDir(name);
    }

    static Assembly FindAssemblyInCurrentDir(string name)
    {
        var files = Directory.GetFiles(Environment.CurrentDirectory, name + ".dll");
        Assembly assembly = null;
        if (files.Length > 0)
        {
            assembly =  Assembly.LoadFile(files[0]);
        }
        if (assembly != null) return assembly;

        if (name.StartsWith("nunit.", StringComparison.OrdinalIgnoreCase) && NUnitDirectory != null && Directory.Exists(NUnitDirectory))
        {
            assembly = FindInDirectory(name, NUnitDirs);
        }

        return assembly;
    }

    static Assembly FindInDirectory(string name, IEnumerable<string> dirs)
    {
        foreach (var dir in dirs)
        {
            //Trace.WriteLine("SEARCHING: " + name + " in: " + dir);
            var files = Directory.GetFiles(dir, name + ".dll");
            if (files.Length > 0) 
            {
                var assembly = Assembly.LoadFile(files[0]);
                return assembly;
            }   
        }
        return null;
    }

    static string GetPartIfAvailable(string str, string key)
    {
        if (str.Contains(key))
        {
            str = str.Replace(key, "");
            str = str.Trim();
            return str;
        }
        return null;
    }

    private static DependentAssemblyInfo GetDependentAssembly(XmlTextReader parentReader) 
    {
        DependentAssemblyInfo info = new DependentAssemblyInfo();
        string content = parentReader.ReadInnerXml();
            StringReader strReader1 = new StringReader(MakeValidXml(content));
        using (XmlTextReader reader = new XmlTextReader(strReader1))
        {
            while (!reader.EOF)
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (String.Compare("assemblyIdentity", reader.Name, StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        info.Name = reader.GetAttribute("name");
                        info.Culture = reader.GetAttribute("culture");
                        info.PublicKey = reader.GetAttribute("publicKeyToken");
                    }
                    else if (String.Compare("codeBase", reader.Name, StringComparison.OrdinalIgnoreCase) == 0) 
                    {
                        info.Version = reader.GetAttribute("version");
                        info.SuggestedPath = reader.GetAttribute("href");
                        if (info.SuggestedPath != null)
                        {
                            info.SuggestedPath = CleanupFilePath(info.SuggestedPath);
                        }
                    }
                }
                reader.Read();
            }
        }
        return info.Name != null && info.SuggestedPath != null? info:null;
    }

    private static string CleanupFilePath(string p)
    {
        var path = p.ToUpperInvariant();
        if (path.Contains("FILE://"))
        {
            path = (new Uri(path).LocalPath);
        }

        path = (new FileInfo(path)).FullName;
        return path;
    }

    static string MakeValidXml(string content)
    {
        return Resources.XmlHeader + Resources.XmlDummyTagStart + content + Resources.XmlDummyTagEnd;
    }
    class DependentAssemblyInfo
    {
        public string Name { get; set; }
        public string PublicKey { get; set; }
        public string Version { get; set; }
        public string Culture { get; set; }
        public string SuggestedPath { get; set; }

        public override string ToString()
        {
            return String.Format("Name={0}; Version={1}; Culture={2}; PublicKey={3}, Path={4}", Name, Version, Culture, PublicKey, SuggestedPath);
        }
    }
    #endregion
}